Python 官方团队在打包项目中踩过的坑
👆 “Python猫” ,一个值得加星标的公众号
在前两篇文章中,我介绍了Python 具有的包类型以及包的构建方式,尤其介绍了 PEP-517/518。尽管这些更改主要是为了使打包变得更健壮,但是在实施和发布时,我们却遇到了一些问题。这篇文章将介绍一部分,希望可以为大家提供经验教训,并提出一些有趣的问题以待将来解决。
查看 PEP-517 和 PEP-518 的改动,可以认为构建后端(亦即 setuptools、flit)几乎没有做什么,只是通过 Python 模块提供了功能接口。大部分繁重的工作都在构建前端上,它需要生成隔离的 Python,然后以新的方式调用构建后端。如今当我们谈论构建前端时,我们的选项主要是 pip 或 poetry(和开发者的 tox)。
这些项目由社区维护,由少数活跃的开发者在空闲时间维护。他们并没有因此获得报酬,而且需要谨慎考虑这些工具被使用的多种方式。考虑到这一点,在 PEP 被接受之后,还花了几乎两年时间才首次实施就不足为奇了。计划、测试和实施已经在背后进行了一年多。
但是,尽管做了所有准备工作,不可避免的是,第一版确实破坏了一些软件包,在大多数情况下,人们做的某些操作使维护人员感到惊讶。让我们试着了解其中一些例子,以及它们是如何被解决的。
Mink Mingle摄/Unsplash--准备好出发!
PEP-518
此 PEP 引入了TOML文件格式。【2】一种专门为了易于读/写配置而创建的格式。尽管在build-system
部分下介绍了打包配置,但其它工具可以自由地将其配置放在tool:name
部分下,因为它们拥有 PyPi 命名空间中的名字。各种工具立即开始利用这一点(例如Towncrier【3】、 black【4】等)。
当pip 18.0(于2018年7月22日发布) 【5】添加对 PEP-518 包的支持时,使用 pyproject.toml 最早出问题,因为 PEP-518 要求所有带 pyproject.toml 的软件包必须指定 build-backend 部分。但是,软件包事先仅将其用于其它项目的配置文件,由于它们没有事先指定它,当 pip 碰到这些文件时,就会引发错误,提示 pyproject.toml 文件无效。
PEP-517
pip wheel 缓存问题
pip 在 PEP-517 世界中的安装方式是首先生成一个 wheel,然后将其提取。要进入 PEP-517 世界,必须指定 build-backend 键,否则每条声明都需要退回到使用 setup.py 命令。
当 pip 构建 wheel 时,默认情况下会通过缓存系统完成。这是一种提速机制,为了在多个虚拟环境需要同一个 wheel 时,我们不用对其进行重建,而是重复使用它。PEP-517 wheel 的构建操作也利用了这一机制。
但是,当你禁用缓存时,这就变得很麻烦。因为没有目标文件夹可用于构建 wheel。所以构建过程将失败,请参阅附录的问题。【6】这个问题虽然很早就显现出来了,但由于大多数 CI 系统都在启用该选项的情况下运行。仅在一天后,pip 19.0.1 修复了该问题。
pyproject.toml 没有加入 setuptools 中
事实证明,构建后端实际上要做的工作不仅仅是 PEP-517 中描述的公开其 API。后端还需要确保 pyproject.toml 被附加到已构建的源码包中,否则用户计算机上的构建后端将无法使用它。setuptools 1650【7】将为setuptools【8】修复此问题,在早期版本中,只需在 MANIFEST.in 中指定 pyproject.toml 即可。
Jorge Zapata摄/Unsplash--什么?!那永远不会发生
从 setup.py 中导入构建的包
另一个意外问题是从 setup.py 内导入软件包时。按照约定,软件包的版本既作为软件包的元数据公开(setup.py 中的 setuptools,setup 函数的 version 参数),也在软件包根目录的__version__ 变量公开。可以在两个地方都指定变量的内容,但是要使其保持同步就很麻烦。
一种解决方法:许多程序包将其放在根目录的 version.py 中,然后同时从 setup.py 和程序包根目录导入它,像这样from mypy.version import __version__ as version
。这能起作用,因为当有人调用 Python 脚本时,当前的工作目录会自动被添加到 sys.path 中(因此你可以导入公开在其下的内容)。
但是,这种添加当前工作目录的行为从来不是强制的,更多的是通过python setup.py sdist
调用构建时,产生的副作用。由于这种行为是副作用(并非保证),因此从 setup.py 导入的所有项目都应在构建开始时,将脚本文件夹显式地添加到 sys 路径。
是否该在打包期间(当尚未构建/分发时)导入已编译的软件包,这尚有争议(尽管 Python 打包组倾向于这样做)。然而,实际上当 setuptools 通过 setuptools.build_meta 暴露其接口时,它选择不把当前工作目录添加到系统路径。
PEP 从未要求后端做此添加,因为大多数构建后端(本质上是声明式的)根本不需要它。因此,此类功能被认为是前端的责任。setuptools 认为,如果用户需要此功能,则应在 setup.py 中明确指出,并提前手动在 sys.path 中添加相应的路径。
为了简化 pip 代码库,pip 决定加入 PEP-517,让所有人在 setuptools 后端加上 pyproject.toml。现在因为这个问题,即使没有选择加入 PEP-517 的程序包也出现崩溃。为了解决这个问题,setuptools 添加了一个新的构建后端(setuptools.build_meta:__ legacy__),当未指定构建后端时,前端可将其用作默认值;当项目添加 build-backend 键时,它们还必须更改其 setup.py,要么将源码根目录添加到 sys.path,要么避免从源码根目录导入。
自举的后端
还出现了另一个有趣的问题,该问题的用户群更加紧密,但是却暴露了一个有趣的问题。如果我们不想使用 wheel,我们只能通过源发行版进行设置;我们应该如何解决”如何提供构建后端的构建后端的问题“?例如,setuptools 通过setuptools 打包自身。也即当 setuptools 通过 PEP-517 指定了这一点时,构建前端将被放入无限循环内。
要安装 pugs 库,它首先会尝试创建一个隔离的环境。这个环境需要 setuptools ,因此构建前端就需要构建一个 wheel 来满足它。wheel 构建本身将触发隔离环境的创建,该环境又依赖于 setuptools。
如何打破这个循环?要求所有构建后端必须暴露为 wheel?允许后端构建自身?这些自建后端是否应该负担依赖项?漫长的各种观点间争论,利与弊,所以如果你有兴趣,请进入python Discourse board【9】,发表你的意见。
Sneaky Elbow摄/Unsplash--我们是一伙的
小结
打包是很难的。在业余时间完善打包系统,使用户可以在打包期间编写和运行任意代码,但还不引起任何破坏,这几乎是不可能的。
现在有了 PEP-518,构建时依赖项是明确的,并且构建环境易于创建。有了 PEP-517,我们可以使用更具声明性的打包命名空间,这减少了用户犯错的可能,当错误不可避免时,也能提供更好的消息。
诚然,在进行这些更改时,某些程序包可能会损坏,并且我们可能令曾经有效的方法失效。但是,我们(PyPa 的维护者)并不是出于恶意而这样做的,因此,当出现错误时,请务必填写详细的错误报告,例如什么错误、你的使用方法,以及你的用例。
我们努力在真诚地改善打包生态系统,为此我们创建了集成测试【10】存储库,以确保将来至少可以捕获到其中的一些边缘用例,免得它们落入到你的机器中。如果你对打包有任何建议或诉求,请随时在“ 讨论Python论坛【11】”的打包部分进行讨论,或者为相关工具提一个 issue。
Milan Popovic摄/ Unsplash--结束了
先到此为止了,谢谢阅读完!我要感谢Paul Ganssle【12】审阅了打包系列文章,并要感谢Tech At Bloomberg【13】允许我在工作期间作开源贡献。
优质文章,推荐阅读: